﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Mdqo.Fx.Text
{
	/// <summary>
	/// Provides extensions for the <see cref="string"/> type.
	/// </summary>
	public static class TextExtensions
	{
		private static readonly Func<string, bool> _isEmpty = string.IsNullOrEmpty;
		private static readonly Func<string, string, bool> _areEqual = StringComparer.OrdinalIgnoreCase.Equals;
		private static readonly IList<TextRule> _pluralRules = GetPluralRules();
		private static readonly IList<TextRule> _singularRules = GetSingularRules();
		private static readonly List<string> _uncountables = new List<string>();

		static TextExtensions()
		{
			AddIrregular("person", "people");
			AddIrregular("man", "men");
			AddIrregular("child", "children");
			AddIrregular("sex", "sexes");
			AddIrregular("move", "moves");
			AddIrregular("stadium", "stadiums");

			AddUncountables("equipment", "information", "rice", "money", "species", "series", "fish", "sheep");
		}

		/// <summary>
		/// Determines whether the current text is equivalent to the specified other text.
		/// </summary>
		/// <param name="text">The text.</param>
		/// <param name="other">The other text.</param>
		/// <returns>
		///   <c>true</c> if the current text is equivalent to the specified other text; otherwise, <c>false</c>.
		/// </returns>
		public static bool IsEquivalentTo(this string text, string other)
		{
			return (_isEmpty(text) && _isEmpty(other)) || _areEqual(text, other);
		}

		/// <summary>
		/// Appends the specified text with optional formatting, if the text is not empty.
		/// Assists in calling <see cref="StringBuilder.AppendFormat(string,object)"/> only
		/// when a value is present, and in calling <see cref="StringBuilder.Append(string)"/>
		/// only when a value is present. Also optionally calls <see cref="StringBuilder.AppendLine()"/>
		/// if text is appended and <paramref name="appendLine"/> is <c>true</c>.
		/// </summary>
		/// <param name="builder">The builder.</param>
		/// <param name="text">The text.</param>
		/// <param name="appendLine">if set to <c>true</c>, append a line after the specified text.</param>
		/// <param name="args">The optional format arguments.</param>
		/// <returns>The builder.</returns>
		public static StringBuilder AppendIfNotEmpty(this StringBuilder builder, string text, bool appendLine = false, params object[] args)
		{
			if (string.IsNullOrEmpty(text)) return builder;
			if (args != null && args.Length > 0) builder.AppendFormat(text, args);
			builder.Append(text);
			return appendLine ? builder.AppendLine() : builder;
		}

		/// <summary>
		/// Converts the specified text to singular form.
		/// </summary>
		/// <param name="text">The text.</param>
		/// <returns>The singular text.</returns>
		public static string ToSingular(this string text)
		{
			return ApplyFormRules(text, _singularRules);
		}

		/// <summary>
		/// Converts the specified text to plural form.
		/// </summary>
		/// <param name="text">The text.</param>
		/// <returns>The plural text.</returns>
		public static string ToPlural(this string text)
		{
			return ApplyFormRules(text, _pluralRules);
		}

		/// <summary>
		/// Converts the text into either its singular or plural form, based on the specified item count.
		/// </summary>
		/// <param name="itemText">The item text.</param>
		/// <param name="itemCount">The item count.</param>
		/// <returns>The altered text.</returns>
		public static string ToProperItemForm(this string itemText, int itemCount)
		{
			return Math.Abs(itemCount) == 1 ? itemText.ToSingular() : itemText.ToPlural();
		}

		private static IList<TextRule> GetPluralRules()
		{
			return new List<TextRule>
			{
				new TextRule("(oxen|octopi|viri|aliases|quizzes)$", "$1"),
				new TextRule("(people|men|children|sexes|moves|stadiums)$", "$1"),
				new TextRule("(quiz)$", "$1zes"),
				new TextRule("^(ox)$", "$1en"),
				new TextRule("([m|l])ice$", "$1ice"),
				new TextRule("([m|l])ouse$", "$1ice"),
				new TextRule("(matr|vert|ind)ix|ex$", "$1ices"),
				new TextRule("(x|ch|ss|sh)$", "$1es"),
				new TextRule("([^aeiouy]|qu)y$", "$1ies"),
				new TextRule("(hive)$", "$1s"),
				new TextRule("(?:([^f])fe|([lr])f)$", "$1$2ves"),
				new TextRule("sis$", "ses"),
				new TextRule("([ti])a$", "$1a"),
				new TextRule("([ti])um$", "$1a"),
				new TextRule("(buffal|tomat)o$", "$1oes"),
				new TextRule("(bu)s$", "$1ses"),
				new TextRule("(alias|status)$", "$1es"),
				new TextRule("(octop|vir)i$", "$1i"),
				new TextRule("(octop|vir)us$", "$1i"),
				new TextRule("(ax|test)is$", "$1es"),
				new TextRule("s$", "s"),
				new TextRule("$", "s")
			};
		}

		private static IList<TextRule> GetSingularRules()
		{
			return new List<TextRule>
			{
				new TextRule("(quiz)zes$", "$1"),
				new TextRule("(matr)ices$", "$1ix"),
				new TextRule("(vert|ind)ices$", "$1ex"),
				new TextRule("^(ox)en", "$1"),
				new TextRule("(alias|status)$", "$1"),
				new TextRule("(alias|status)es$", "$1"),
				new TextRule("(octop|vir)us$", "$1us"),
				new TextRule("(octop|vir)i$", "$1us"),
				new TextRule("(cris|ax|test)es$", "$1is"),
				new TextRule("(cris|ax|test)is$", "$1is"),
				new TextRule("(shoe)s$", "$1"),
				new TextRule("(o)es$", "$1"),
				new TextRule("(bus)es$", "$1"),
				new TextRule("([m|l])ice$", "$1ouse"),
				new TextRule("(x|ch|ss|sh)es$", "$1"),
				new TextRule("(m)ovies$", "$1ovie"),
				new TextRule("(s)eries$", "$1eries"),
				new TextRule("([lr])ves$", "$1f"),
				new TextRule("(tive)s$", "$1"),
				new TextRule("(hive)s$", "$1"),
				new TextRule("([^f])ves$", "$1fe"),
				new TextRule("(^analy)sis$", "$1sis"),
				new TextRule("(^analy)ses$", "$1sis"),
				new TextRule("((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$", "$1$2sis"),
				new TextRule("([ti])a$", "$1um"),
				new TextRule("(n)ews$", "$1ews"),
				new TextRule("(s|si|u)s$", "$1s"),
				new TextRule("s$", "")
			};
		}

		private static void AddIrregular(string singular, string plural)
		{
			string singularRemainder = singular.Length > 1 ? singular.Substring(1) : string.Empty;
			string pluralRemainder = plural.Length > 1 ? plural.Substring(1) : string.Empty;
			const string ruleFormat = "({0}){1}$";
			const string replacementFormat = "$1{0}";
			_pluralRules.Insert(0, new TextRule(string.Format(ruleFormat, singular.First(), singularRemainder), string.Format(replacementFormat, pluralRemainder)));
			_singularRules.Insert(0, new TextRule(string.Format(ruleFormat, plural.First(), pluralRemainder), string.Format(replacementFormat, singularRemainder)));
		}

		private static void AddUncountables(params string[] terms)
		{
			_uncountables.AddRange(terms.Select(term => term.Trim()));
		}

		private static bool IsUncountable(string term)
		{
			return _uncountables.Any(item => StringComparer.OrdinalIgnoreCase.Equals(item, term));
		}
		
		private static string ApplyFormRules(string text, IEnumerable<TextRule> rules)
		{
			string targetText = text.Trim();

			if (string.IsNullOrEmpty(targetText) || IsUncountable(targetText))
				return targetText;

			return rules.Select(rule => rule.Apply(targetText))
			       	.Where(item => !string.IsNullOrEmpty(item))
			       	.FirstOrDefault() ?? text;
		}
	}
}